Skip to content

fix: wire keepalive configuration fields into gRPC server options#399

Merged
mangelajo merged 2 commits intojumpstarter-dev:mainfrom
raballew:030-wire-keepalive-grpc
Apr 6, 2026
Merged

fix: wire keepalive configuration fields into gRPC server options#399
mangelajo merged 2 commits intojumpstarter-dev:mainfrom
raballew:030-wire-keepalive-grpc

Conversation

@raballew
Copy link
Copy Markdown
Member

Summary

  • The five keepalive config fields (Timeout, IntervalTime, MaxConnectionIdle, MaxConnectionAge, MaxConnectionAgeGrace) were parsed from configuration but never wired into keepalive.ServerParameters, making them silently ignored
  • LoadGrpcConfiguration now returns []grpc.ServerOption and constructs a ServerParameters option when any keepalive field is non-zero
  • Propagates the slice type change through all callers (controller service, router service, cmd entrypoints)

Test plan

  • 8 new tests covering: timeout/interval wiring, connection lifetime fields, invalid value errors for all 5 fields, empty fields produce zero-value params, backward compatibility
  • All existing config tests pass with no regressions
  • Full go build ./... clean

Closes #363

🤖 Generated with Claude Code

The five keepalive config fields (Timeout, IntervalTime,
MaxConnectionIdle, MaxConnectionAge, MaxConnectionAgeGrace) were parsed
from configuration but never passed to keepalive.ServerParameters,
making them silently ignored.

Changes LoadGrpcConfiguration to return []grpc.ServerOption and
constructs a ServerParameters option when any keepalive field is
non-zero. Propagates the slice type change through all callers.

Closes jumpstarter-dev#363

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 31, 2026

Deploy Preview for jumpstarter-docs ready!

Name Link
🔨 Latest commit 8404dfc
🔍 Latest deploy log https://app.netlify.com/projects/jumpstarter-docs/deploys/69cd135ac6c14000083a397e
😎 Deploy Preview https://deploy-preview-399--jumpstarter-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e73182f-9b9a-44d3-be9c-8fc30c3abd29

📥 Commits

Reviewing files that changed from the base of the PR and between 833a9c4 and 8404dfc.

📒 Files selected for processing (3)
  • controller/cmd/main.go
  • controller/internal/service/controller_service.go
  • controller/internal/service/router_service.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • controller/cmd/main.go
  • controller/internal/service/router_service.go
  • controller/internal/service/controller_service.go

📝 Walkthrough

Walkthrough

The code refactors gRPC server configuration handling to support multiple grpc.ServerOption values instead of single options. Configuration loaders now return slices of server options, services store ServerOptions []grpc.ServerOption, and startup paths assemble and pass combined option slices to grpc.NewServer().

Changes

Cohort / File(s) Summary
Config Loading
controller/internal/config/config.go, controller/internal/config/grpc.go
Changed loaders to return []grpc.ServerOption. LoadGrpcConfiguration now parses additional keepalive duration fields (Timeout, IntervalTime, MaxConnectionIdle, MaxConnectionAge, MaxConnectionAgeGrace), builds keepalive.ServerParameters when present, accumulates multiple grpc.ServerOption entries, and returns detailed parse errors for invalid durations.
Services
controller/internal/service/controller_service.go, controller/internal/service/router_service.go
Replaced ServerOption grpc.ServerOption with ServerOptions []grpc.ServerOption. Start() now appends interceptors/credentials to s.ServerOptions and calls grpc.NewServer(opts...) with the combined slice.
Command Entrypoints
controller/cmd/main.go, controller/cmd/router/main.go
Updated struct literals to set ServerOptions: option (slice) instead of the previous singular ServerOption: option.
Tests
controller/internal/config/grpc_test.go
Added unit tests covering various Keepalive inputs: option counts, defaulting behavior, and error messages for invalid duration fields.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as Main (cmd)
    participant Config as Config Loader
    participant Service as Router/Controller Service
    participant gRPC as gRPC Server

    CLI->>Config: LoadConfiguration / LoadGrpcConfiguration
    Config-->>CLI: []grpc.ServerOption (enforcement, params...)
    CLI->>Service: construct service with ServerOptions slice
    Service->>Service: append interceptors/credentials to options
    Service->>gRPC: grpc.NewServer(opts...)
    gRPC-->>Service: server running
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • NickCao

Poem

🐰 whiskers twitch with glee,
options gathered, snug as can be.
Keepalive threads now linked in line,
servers hum and configs shine,
hop, deploy — all set to be! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main objective: wiring keepalive configuration fields into gRPC server options.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining the problem, solution, and test coverage.
Linked Issues check ✅ Passed The changes fully implement the requirements from issue #363 by wiring the five keepalive fields into keepalive.ServerParameters and passing as gRPC server options.
Out of Scope Changes check ✅ Passed All changes are directly related to wiring keepalive configuration; no unrelated modifications are present in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
controller/internal/service/controller_service.go (1)

941-959: Ordering difference from router_service.go.

In this file, user-configured options (s.ServerOptions) are placed first via append(s.ServerOptions, interceptors...), whereas router_service.go places defaults first and appends user options. While this likely doesn't cause functional issues (gRPC options are typically additive rather than overriding), consider aligning the ordering across both services for consistency and easier maintenance.

♻️ Optional: Align ordering with router_service.go
-	opts := append(s.ServerOptions,
+	opts := []grpc.ServerOption{
 		grpc.ChainUnaryInterceptor(func(
 			gctx context.Context,
 			req any,
 			_ *grpc.UnaryServerInfo,
 			handler grpc.UnaryHandler,
 		) (resp any, err error) {
 			return handler(logContext(gctx), req)
 		}, recovery.UnaryServerInterceptor()),
 		grpc.ChainStreamInterceptor(func(
 			srv any,
 			ss grpc.ServerStream,
 			_ *grpc.StreamServerInfo,
 			handler grpc.StreamHandler,
 		) error {
 			return handler(srv, &wrappedStream{ServerStream: ss})
 		}, recovery.StreamServerInterceptor()),
-	)
+	}
+	opts = append(opts, s.ServerOptions...)
 	server := grpc.NewServer(opts...)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/internal/service/controller_service.go` around lines 941 - 959,
The append ordering of server options is inconsistent with router_service.go:
currently opts := append(s.ServerOptions, grpc.ChainUnaryInterceptor(...),
grpc.ChainStreamInterceptor(...)) places user options first; change it to
construct opts by starting with the default interceptors (the
grpc.ChainUnaryInterceptor and grpc.ChainStreamInterceptor entries) and then
append s.ServerOptions so defaults come first, ensuring the same ordering used
when creating the server via grpc.NewServer; update the block that builds opts
(references: s.ServerOptions, grpc.ChainUnaryInterceptor,
grpc.ChainStreamInterceptor, grpc.NewServer) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@controller/internal/service/controller_service.go`:
- Around line 941-959: The append ordering of server options is inconsistent
with router_service.go: currently opts := append(s.ServerOptions,
grpc.ChainUnaryInterceptor(...), grpc.ChainStreamInterceptor(...)) places user
options first; change it to construct opts by starting with the default
interceptors (the grpc.ChainUnaryInterceptor and grpc.ChainStreamInterceptor
entries) and then append s.ServerOptions so defaults come first, ensuring the
same ordering used when creating the server via grpc.NewServer; update the block
that builds opts (references: s.ServerOptions, grpc.ChainUnaryInterceptor,
grpc.ChainStreamInterceptor, grpc.NewServer) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 593c602c-894a-4978-88e9-b03ec3d765ad

📥 Commits

Reviewing files that changed from the base of the PR and between 03fc412 and 833a9c4.

📒 Files selected for processing (7)
  • controller/cmd/main.go
  • controller/cmd/router/main.go
  • controller/internal/config/config.go
  • controller/internal/config/grpc.go
  • controller/internal/config/grpc_test.go
  • controller/internal/service/controller_service.go
  • controller/internal/service/router_service.go

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mangelajo
Copy link
Copy Markdown
Member

mangelajo commented Apr 6, 2026

@coderabbitai can you verify if this patch could potentially change behavior of any server using the default settings?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me examine the relevant source files to answer this properly.


🏁 Script executed:

#!/bin/bash
fd -e go grpc.go --search-path controller
cat controller/internal/config/grpc.go

echo "---"
fd -e go grpc_test.go --search-path controller
cat controller/internal/config/grpc_test.go

echo "---"
cat controller/internal/config/types.go

Length of output: 10463


Now let me also check the backward compatibility path in config.go:


🏁 Script executed:

#!/bin/bash
cat controller/internal/config/config.go

Length of output: 2957


@mangelajo — after reviewing grpc.go, config.go, types.go, and the test suite, the patch does not change behavior for servers using default settings. Here's the breakdown:


Default settings path (Keepalive all empty / zero)

The critical guard is here in grpc.go:

params := keepalive.ServerParameters{
    Timeout:               timeout,       // 0
    Time:                  intervalTime,  // 0
    MaxConnectionIdle:     maxConnectionIdle,   // 0
    MaxConnectionAge:      maxConnectionAge,    // 0
    MaxConnectionAgeGrace: maxConnectionAgeGrace, // 0
}

if params != (keepalive.ServerParameters{}) {   // false → skipped
    options = append(options, grpc.KeepaliveParams(params))
}

Because ParseDuration("") returns 0 for each empty field, the struct comparison is always false when no fields are configured, so grpc.KeepaliveParams is never appended. The function returns only [grpc.KeepaliveEnforcementPolicy(...)] — exactly the same single option as before. This is also directly validated by TestLoadGrpcConfigurationEmptyKeepaliveStructUsesDefaults and TestLoadGrpcConfigurationEmptyKeepaliveFieldsReturnZeroValueParams.

Backward compatibility path (old authentication key in ConfigMap)

The hardcoded path in config.go is also unchanged:

return authenticator, prefix, router, []grpc.ServerOption{
    grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
        MinTime:             1 * time.Second,
        PermitWithoutStream: true,
    }),
}, &Provisioning{Enabled: false}, nil

⚠️ One documentation concern worth flagging

The comments in types.go claim:

// Timeout is the duration to wait for a keepalive ping acknowledgment.
// Default: "180s"
// IntervalTime is the duration between keepalive pings.
// Default: "10s"

These defaults are not programmatically enforced — when the fields are left empty, the code produces 0 (not 180s/10s), and the zero-value guard skips KeepaliveParams entirely, leaving the gRPC library's own internal defaults in effect (Timeout=20s, Time=infinity). The comments are misleading and could cause operators to believe those values are applied when fields are omitted. It would be worth correcting those comments to reflect actual behavior.

Copy link
Copy Markdown
Member

@mangelajo mangelajo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just one nit.

}
if minTime == 0 {
minTime = 1e9 // 1 second default
minTime = 1e9
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep the comment here, it's not obvious that 1e9 means s second in this context. May be there is a constant we can rely on.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a nit...

@mangelajo mangelajo merged commit 647aff9 into jumpstarter-dev:main Apr 6, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keepalive configuration fields parsed but never wired

2 participants